package gazzera.node.core;

import gazzera.gui.*;
import gazzera.gui.messages.*;
import gazzera.node.core.events.*;
import gazzera.node.core.messages.*;
import gazzera.node.core.peer.*;
import gazzera.node.core.plugins.*;
import gazzera.node.core.plugins.download.*;
import gazzera.node.core.plugins.shareplugin.*;
import gazzera.node.core.plugins.trust.*;
import gazzera.node.core.plugins.upload.*;
import gazzera.node.sslserver.*;
import gazzera.utils.*;

import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;
import java.util.concurrent.*;

import javax.net.ssl.*;

import sun.security.x509.*;

import com.sleepycat.je.*;


/**
 * The main core of Gazzera, where all the functionality take place.
 * Note: all the handle* methods run in the same main thread 
 * @author 
 *
 */
public class Core implements Runnable, CertificateAcceptor, ICore, GuiInterface
{
	public static final int MAJOR_VERSION = 2;
	public static final int MINOR_VERSION = 0;
	
	
	private BlockingQueue<Event> eventQueue = new ArrayBlockingQueue<Event>(1000);
	private Server server;
	private List<Peer> peers = new ArrayList<Peer>();
	private String hash;
	private PluginManager pm;
	private Peoples peoples;
	private GuiInterface gui;
	private Environment env;
	private long rightThreadId = 0;
	private TaskManager taskManager = new TaskManager(this); 
	private Person ourPerson;
	private Config config;
	
	
	private String debugGetMyIp() 
	{
		try
		{
		    return java.net.InetAddress.getLocalHost().getHostAddress();
		}
		catch (Exception e)
		{
		    // TODO Auto-generated catch block
		    e.printStackTrace();
		    return "";
		}

		/*
		java.net.URL URL = new java.net.URL("http://www.whatismyip.org/");

		java.net.HttpURLConnection Conn = (HttpURLConnection) URL.openConnection();

		java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(Conn.getInputStream()));
		String ip = null;
		try
		{
			ip = br.readLine();
			System.out.print("Your IP address is " + ip);

		}
		finally
		{
			br.close();
		}
		return ip;
		*/
	}
	
	public Core(File keystore, GuiInterface gui) throws Exception
	{
	    this.gui = gui;
	    
	    // inform the Core we are starting
	    gui.pass(new Log("Starting core"));
	    boolean created = !keystore.exists(); 
	    if(created)
	    {
		// eventually create a new keystore if it wasn't present
	    	createKeystore(keystore);
	    }
	    
	    hash = calculateHash(keystore, "mypass".toCharArray());
	    
	    config = new Config(hash);
	    int port = Integer.parseInt(config.get("core.port", "4444"));
	    
	    // let's first open the server, as it can detect another open node on the same port
	    // (by throwing an Address already bind exception)
	    server = new Server(port, keystore, "mypass".toCharArray(), this, new ConnectionListener()
		{
			@Override
			public boolean acceptConnection(SSLSocket connection, Object o) throws Exception
			{
				enqueue(new ConnectionEvent(connection, (Person)o));
				return true;
			}
		});
	    
	    
	    if(!server.getHash().equals(hash))
	    {
	    	throw new Exception("(Debug) Hash different!!");
	    }
	    
	    // generate a Person with our data.
	    ourPerson = new Person();
	    ourPerson.hash = hash;
	    ourPerson.nickname = config.get("nickname", "NotSet");
	    ourPerson.status = FriendStatus.TRUSTED;
	    ourPerson.port = port;
	    ourPerson.host = "localhost"; // TODO calculate our public IP
	    ourPerson.onlineStatus = OnlineStatus.OFFLINE;
	    
	    setupDatastore(new File(config.getMainFolder(), "store"), false);
	    
	    peoples = new Peoples(config.getMainFolder());
	    if(created)
	    {
			// if this is a new node, add the example friend
			createStandardFriend(gui);
	    }
	    
	    
	    pm = new PluginManager();
		
	    // install plugins. Eventually this can be made dinamic
	    pm.install(new PeopleManagerPlugin());
	    pm.install(new TrustPlugin());
	    pm.install(new DemoPlugin());
	    pm.install(new SharePlugin());
	    pm.install(new DownloadPlugin());
	    pm.install(new UploadPlugin());
		
	    // initialize the plugins.
	    rightThreadId = Thread.currentThread().getId(); // init require access to thread-limited methods
	    pm.init(this);
	    rightThreadId = 0;
		
	    // start accepting SSL requests
	    server.runAsThread();
		
	    gui.pass(new Intro(getHash(), getOurPerson().nickname, debugGetMyIp(), port, config.getMainFolder().getAbsolutePath()));
	    gui.pass(new Log("Core started"));
	    
	    // notify plugins that they can start their mess
	    enqueue(new CoreStarted());
	}

	private void createStandardFriend(GuiInterface gui) throws Exception
	{
		if (ourPerson.hash.equals(new Hash("7002d187e4f190af535e4dbf79a66063d5b8a367")))
		{
		    // we are the standard friend ?! hooray!
		    return;
		}
		
		Person p = peoples.get(hash);
		if ( p == null)
		{
			p = new Person();
			p.hash = "7002d187e4f190af535e4dbf79a66063d5b8a367";
			p.nickname = "ExampleFriend";
			p.status = FriendStatus.FRIEND;
			p.onlineStatus = OnlineStatus.OFFLINE;
			p.port = 7755;
			p.host = "msx.homelinux.org";
			peoples.add(p);
			peoples.save();
		}
	}
	
	public Environment getEnvironment()
	{
		return env;
	}
	
	private void doCreateCert2(KeyStore ks) throws Exception
	{
		CertAndKeyGen cakg = new CertAndKeyGen("RSA", "MD5WithRSA");
        cakg.generate(1024);
        
        //PublicKey publicKey = cakg.getPublicKey();
        //System.out.println(publicKey);
        
        PrivateKey privateKey = cakg.getPrivateKey();
        //System.out.println(privateKey);
        
        X500Name name = new X500Name("Unknown", "Unknown", "Unknown", "Unknown", "Unknown", "Unknown");
        //System.out.println(name);
        
        //X509Cert cert = cakg.getSelfCert(name, 2000000); //deprecated
        java.security.cert.X509Certificate certificate = cakg.getSelfCertificate(name,60 * 60 * 24 * 365 * 10);
        //System.out.println("cert: "+certificate);
        
        //X509Certificate certificate = X509Certificate.getInstance(cert.getSignedCert());
        certificate.checkValidity();
        System.out.println("Issuer DN .......... " + certificate.getIssuerDN());
        System.out.println("Not after .......... " + certificate.getNotAfter());
        System.out.println("Not before ......... " + certificate.getNotBefore());
        System.out.println("Serial No. ......... " + certificate.getSerialNumber());
        System.out.println("Signature Alg. ..... " + certificate.getSigAlgName());
        System.out.println("Signature Alg. OID . " + certificate.getSigAlgOID());
        System.out.println("Subject DN ......... " + certificate.getSubjectDN());
        
		ks.setKeyEntry("ssltestserver", privateKey, "mypass".toCharArray(), new java.security.cert.Certificate[] { certificate });
	}

	
	private void createKeystore(File keystore) throws Exception
	{
		gui.pass(new Log("Generating new identity..."));
		// Security.addProvider(new BouncyCastleProvider());
		Provider[] p = Security.getProviders();
		for (int i = 0; i < p.length; i++)
		{
			System.out.println(p[i].getName()+"  "+p[i]);
		}
		String kstype = KeyStore.getDefaultType();
		KeyStore ks = KeyStore.getInstance(kstype);
		ks.load(null, null);

		//doCreateCert(ks);
		doCreateCert2(ks);
		
		FileOutputStream o=null;
		try
		{
			o = new FileOutputStream(keystore);
			ks.store(o, "mypass".toCharArray());
		}
		finally
		{
			try{o.close();}catch (Exception e){}
		}
		
		String x = calculateHash(keystore, "mypass".toCharArray());
		if (!RegExp.match("x@"+x))
		{
			keystore.delete();
			throw new Exception("Invalid gid created!!");
		}		
		
	}

	/*
	  use bouncy castle
	private void doCreateCert(KeyStore ks) throws NoSuchAlgorithmException, CertificateEncodingException, NoSuchProviderException, SignatureException, InvalidKeyException, KeyStoreException
	{
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
		keyPairGenerator.initialize(1024);
		KeyPair KPair = keyPairGenerator.generateKeyPair();
		X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
		v3CertGen.setSerialNumber(BigInteger.valueOf(new SecureRandom().nextInt(Integer.MAX_VALUE)));
		v3CertGen.setIssuerDN(new X509Principal("CN=none, OU=None, O=None L=None, C=None"));
		v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30));
		v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10)));
		v3CertGen.setSubjectDN(new X509Principal("CN=none, OU=None, O=None L=None, C=None"));

		v3CertGen.setPublicKey(KPair.getPublic());
		v3CertGen.setSignatureAlgorithm("MD5WithRSAEncryption");

		X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(), "BC");

		ks.setKeyEntry("ssltestserver", KPair.getPrivate(), "mypass".toCharArray(), new java.security.cert.Certificate[] { PKCertificate });
	}
	*/
	private void setupDatastore(File envHome, boolean readOnly) 
	 {	
		if (!envHome.exists())
			envHome.mkdir();
		EnvironmentConfig myEnvConfig = new EnvironmentConfig();

		myEnvConfig.setReadOnly(readOnly);

		myEnvConfig.setAllowCreate(!readOnly);
		myEnvConfig.setCacheSize(10*1024*1024);

		env = new Environment(envHome, myEnvConfig);
	 }
	
	public void connect(final String host, final int port, final Person person)
	{
	    debugCheckThread();
	    
	    if(person!=null)
	    {
	    	// notify that we are connecting
	    	person.onlineStatus = OnlineStatus.CONNECTING;
	    	PeopleManagerPlugin.updatePerson(gui, person);
	    }
	    
	    Runnable r = new Runnable()
		{
			@Override
			public void run()
			{
				boolean b;
				try
				{
				    b = Core.this.server.connect(host, port, person);
				}
				catch (Exception e)
				{
				    b = false;
				}
				if(!b)
				{
					Core.this.enqueue(new ConnectionFailedEvent(host, port, person));
				}
			}
		};
		ThreadRunner.start(r, "Connector", true);
		
	}
	
	public void run()
	{
	    rightThreadId = Thread.currentThread().getId();
	    consumeEvents();
	}
	
	
	

	
	/**
	 * A message arrived from the Gui.
	 * Enqueue it in main queue
	 */
	@Override
	public void pass(GuiMessage msg)
	{
		enqueue(new GuiCommandEvent(msg));
	}

	@Override
	public boolean check(X509Certificate cert, SSLSocket socket)
	{
		try
		{
			boolean acceptAll = config.get("accept.stranger.as.friends", "false").equals("true");
		    String hash = SSLUtils.getSha1Cert(cert);
		    if (hash.equals(getHash()))
		    {
		    	logErr("Connecting with ourself");
		    	return false;
		    }
		    gui.pass(new Log("Accepting: "+hash));
		    Person p = peoples.get(hash);
		    if ( p == null)
		    {
		    	p = new Person();
				p.hash = hash;
				p.nickname = "Unknown";
				p.status = acceptAll ? FriendStatus.FRIEND : FriendStatus.STRANGER;
				p.onlineStatus = OnlineStatus.OFFLINE;
				p.port = 0;
				p.host = socket.getInetAddress().getHostAddress();
				peoples.add(p);
				peoples.save();
				
				PeopleManagerPlugin.updatePerson(gui, p);
		    }
		    
		    TrustPlugin t = getPlugin(TrustPlugin.class);
		    
		    return (p.status == FriendStatus.FRIEND) || (p.status == FriendStatus.TRUSTED) || t.isConnectable(hash);
		}
		catch (Exception e)
		{
			logErr(e);
			return false;
		}
	}
	
	/**
	 * This is the main loop consuming events.
	 */
	private void consumeEvents()
	{
	    debugCheckThread();
		try
		{
			boolean done = false;
			while(!done)
			{
				Event e = eventQueue.take();
				if(e instanceof MessageEvent)
				{
					MessageEvent m = (MessageEvent)e;
					handleMessage(m.node, m.message);
				}
				else if(e instanceof ConnectionEvent)
				{
					handleConnection( ((ConnectionEvent)e).socket, ((ConnectionEvent)e).person ); 
				}
				else if(e instanceof ConnectionFailedEvent)
				{
					handleConnectionFailed( ((ConnectionFailedEvent)e).host, ((ConnectionFailedEvent)e).port, ((ConnectionFailedEvent)e).person ); 
				}
				else if(e instanceof DisconnectionEvent)
				{
					handleDisconnection( ((DisconnectionEvent)e).node );
				}
				else if(e instanceof DataPumpTerminated)
				{
					handleDataPumpTerminated( (DataPumpTerminated)e );
				}
				else if(e instanceof CoreStarted)
				{
					handleCoreStarted();
				}
				else if(e instanceof QuitEvent)
				{
					handleQuit();
					done = true;
				}
				else if(e instanceof GuiCommandEvent)
				{
				    handleGuiCommand( ((GuiCommandEvent)e).msg);
				}
				else if (e instanceof TaskTerminatedEvent)
				{
					handleTaskTerminated(((TaskTerminatedEvent) e).task, ((TaskTerminatedEvent) e).interrupted);
				}
			}
		}
		catch (InterruptedException e)
		{
			logErr(e);
		}
	}
	private void handleCoreStarted()
	{
		pm.start();
	}
	private void handleConnection(SSLSocket socket, Person connectee)
	{
	    debugCheckThread();
	    
		try
		{
			Peer p = new Peer(socket);
			Person person;
			if (peers.contains(p))
			{
				person = peoples.get(p.getHash());
				gui.pass(new Log("Peer already connected, closing "+p.getHash()));
				socket.close();
			}
			else
			{
				gui.pass(new Log("New peer connected as "+p.getHash()));
				
				peers.add(p);
				
				person = peoples.get(p.getHash());
				person.host = ((InetSocketAddress)socket.getRemoteSocketAddress()).getAddress().getHostAddress();
				person.onlineStatus = OnlineStatus.ONLINE;
				PeopleManagerPlugin.updatePerson(gui, person);
								
				p.start(this);
											
				pm.nodeConnected(p);
			}
			
			if(connectee != null)
			{
				// we were trying to connect with someone specific
				if(!connectee.hash.equals(person.hash))
				{
					// for some reason, trying to connect with someone
					// ended up being connected to someone else.
					// update the status of the original person
					if(connectee.onlineStatus == OnlineStatus.CONNECTING)
					{
						connectee.onlineStatus = OnlineStatus.OFFLINE;
						PeopleManagerPlugin.updatePerson(gui, connectee);
					}
				}
				else
				{
					// we already updated above
				}
			}
			
		}
		catch (Exception e)
		{
			try	{ socket.close(); } catch (IOException e1){}
		}
	}
	private void handleConnectionFailed(String host, int port, Person person)
	{
		debugCheckThread();

		if (person != null)
		{
			if (person.onlineStatus == OnlineStatus.CONNECTING)
			{
				person.onlineStatus = OnlineStatus.OFFLINE;
				PeopleManagerPlugin.updatePerson(gui, person);
			}
		}
		else
		{
			gui.pass(new Log("Unable to connect to "+host+":"+port));
		}
		
				
	
	}
	private void handleDisconnection(Node node)
	{
	    debugCheckThread();
	    gui.pass(new Log("disconnected  "+node.getHash()));
	    Person person = peoples.get(node.getHash());
		person.onlineStatus = OnlineStatus.OFFLINE;
		PeopleManagerPlugin.updatePerson(gui, person);
		
		peers.remove(node);
		
		node.close();
		pm.nodeDisconnected(node);
	}
	private void handleMessage(Node node, Message msg)
	{
	    debugCheckThread();
	    pm.message(node, msg);
	}
	private void handleDataPumpTerminated( DataPumpTerminated e )
	{
	    debugCheckThread();
	    gui.pass(new Log("Datapump terminated "+e.src.getClass().getSimpleName()+" for "+e.node));
		pm.dataPumpTerminated(e.node, e.src);
	}
	private void handleTaskTerminated(Task task, boolean interrupted)
	{
		debugCheckThread();
		taskManager.terminated(task);
		pm.taskTerminated(task, interrupted);
		
	}
	private void handleGuiCommand(GuiMessage msg)
	{
	    
		pm.command(msg);
		if(msg instanceof Quit)
		{
		    enqueue(new QuitEvent());
		}
	}
	
	private void handleQuit()
	{
	    debugCheckThread();
	    gui.pass(new Log("Initiating shutdown sequence"));

	    gui.pass(new Log("Closing server"));
	    server.close();
	    
	    gui.pass(new Log("Disconnecting peers"));
		for (Peer p : peers)
		{
		    p.close();
		}
		gui.pass(new Log("Clearing event queue"));
		eventQueue.clear();
		
		// TODO should close datapumps too!
		
		gui.pass(new Log("Terminating tasks"));
		taskManager.terminate();
			
		gui.pass(new Log("Finalizing plugins"));
		pm.finish();
		gui.pass(new Log("Closing database"));
		env.close();
		gui.pass(new Log("Shutdown completed"));
		
	}
	
	public Peoples getPeoples()
	{
		debugCheckThread();
		return peoples;
	}
	
	
	public void broadcast(Message m, Node... excluded)
	{
	    debugCheckThread();
		List<Node> ex = Arrays.asList(excluded);
		for (Peer p : peers)
		{
			try
			{
				if (!ex.contains(p))
				{
					p.enqueue(m);
				}
			}
			catch (Exception e)
			{
				logErr(e);
			}
		}
	}
	
	public Node find(String hash)
	{
	    debugCheckThread();
		for (Node p : peers)
		{
			if (p.getHash().equals(hash))
			{
				return p;
			}
		}
		return null;
	}
/*
	private void close() throws Exception
	{
	    enqueue(new QuitEvent());
	}*/
	public String getHash()
	{
	    return hash;
	}
	public void debugCheckThread()
	{
	    if(Thread.currentThread().getId() != rightThreadId)
	    {
	    	logErr("WRONG THREAD!! "+rightThreadId+" - "+Thread.currentThread().getId(), new Exception());
	    	throw new RuntimeException("WRONG THREAD");
	    }
	}
	
	public void enqueue(Event event)
	{
		try
		{
			eventQueue.put(event);
		}
		catch (Exception e)
		{
			logErr(e);
		}
	}
	
	private String calculateHash(File keystore, char[] password) throws Exception
	{
		//KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
		String kstype= KeyStore.getDefaultType();
	    KeyStore ks = KeyStore.getInstance(kstype);

	    FileInputStream fis = new FileInputStream(keystore);
	    ks.load(fis, password);
	    fis.close();
	    //kmf.init(ks, password);
	    Certificate x = ks.getCertificate("ssltestserver");
	    hash = SSLUtils.getSha1Cert(x);
	    return hash;	
	}
	
	public void logErr(String s)
	{
		System.err.println(s);
	}
	public void logErr(String s, Throwable t)
	{
		System.err.println(s);
		t.printStackTrace(System.err);
	}
	public void logErr(Throwable t)
	{
		logErr(t.getMessage(), t);
	}
	public GuiInterface getGui()
	{
		return gui;
	}

	
	public <T extends Plugin> T getPlugin(Class<T> cls)
	{
		return pm.getPlugin(cls);
	}

	@Override
	public void startTask(Task task)
	{
		taskManager.startTask(task);
		
	}
	public Person getOurPerson()
	{
		return ourPerson;
	}
	public Config getConfig()
	{
		return config;
	}
	public boolean isBusy()
	{
	    return eventQueue.size()>500;
	}
}
